package com.yaoyong.codegenerator.utils;

import com.yaoyong.codegenerator.entity.Column;
import com.yaoyong.codegenerator.entity.Database;
import com.yaoyong.codegenerator.entity.Table;
import com.yaoyong.codegenerator.properties.DatabaseProperties;
import org.springframework.beans.factory.annotation.Autowired;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

/**
 * 操作数据库的类[仅支持mysql和oracle数据库],主要包括如下功能:
 * 1.查看所有数据库列表 -> List<String> showDatabases();
 * 2.查看指定数据库 -> Database getDatabase(String dbName);
 * 3.查看指定数据库下所有表 -> List<String> showTables(String dbName);
 * 4.查看指定数据库的所有表结构 -> List<Table> getTables(String dbName);
 *
 * @author 姚勇
 */
public class DatabaseUtil {

    /**
     * 缓存
     */
    public static final Map<String, Database> dbContext = new HashMap<>();

    @Autowired
    private DatabaseProperties databaseProperties;

    public Database getDatabase(String dbName) throws SQLException {
        Database database;
        if (dbContext.get(dbName) == null) {
            database = createDatabase(dbName);
            dbContext.put(dbName, database);
        } else {
            database = dbContext.get(dbName);
        }
        return database;
    }

    private Database createDatabase(String dbName) throws SQLException {
        Database database = new Database();
        database.setName(dbName);
        database.setDatabaseType(databaseProperties.getDatabaseType());
        database.setDriverClassName(databaseProperties.getDriverClassName());
        database.setUrl(databaseProperties.getUrl().replace("[dbName]",dbName));
        database.setUsername(databaseProperties.getUsername());
        database.setPassword(databaseProperties.getPassword());
        database.setTables(getTables(database));
        return database;
    }


    /**
     * 获取所有数据库名[show databases]
     */
    public List<String> showDatabases() throws SQLException {
        return showDatabases(getConnection());
    }

    /**
     * 获取所有数据库名[show databases]
     */
    public List<String> showDatabases(Connection connection) throws SQLException {
        ResultSet catalogs = null;
        ResultSet schemas = null;
        try {
            List<String> list = new ArrayList<String>();
            // 获取连接
            if (connection == null) {
                connection = getConnection();
            }
            // 获取数据库元数据
            DatabaseMetaData metaData = connection.getMetaData();
            // 获取可在此数据库中使用的类别名称 show databases
            catalogs = metaData.getCatalogs();
            while (catalogs.next()) {
                list.add(catalogs.getString("TABLE_CAT"));
            }
            if (list.isEmpty()) {
                //oracle数据库schem对应表空间
                schemas = metaData.getSchemas();
                while (schemas.next()) {
                    list.add(schemas.getString("TABLE_SCHEM"));
                }
            }

            return list;
        } finally {
            close(schemas);
            close(catalogs);
            close(connection);
        }
    }

    /**
     * 获取指定数据库中的所有表
     */
    public List<String> showTables(String dbName) throws SQLException {
        ArrayList<String> list = new ArrayList();
        Database database = getDatabase(dbName);
        List<Table> tables = database.getTables();
        for (Table table : tables) {
            list.add(table.getName());
        }
        return list;
    }

    public List<Table> getTables(String dbName) throws SQLException {
        return getDatabase(dbName).getTables();
    }


    /**
     * 获取表及字段信息
     * <p>
     * 数据库catalog和schema的区别
     * 按照SQL标准的解释，在SQL环境下Catalog和Schema都属于抽象概念，主要用来解决命名冲突问题。
     * 从概念上说，一个数据库系统包含多个Catalog，每个Catalog又包含多个Schema，而每个Schema又包含多个数据库对象（表、视图、序列等），反过来讲一个数据库对象必然属于一个Schema，而该Schema又必然属于一个Catalog，这样我们就可以得到该数据库对象的完全限定名称从而解决命名冲突的问题了
     * 从实现的角度来看，各种数据库系统对Catalog和Schema的支持和实现方式千差万别，针对具体问题需要参考具体的产品说明书，比较简单而常用的实现方式是使用数据库名作为Catalog名，Oracle使用用户名作为Schema名
     * 例如:
     * 数据库          Catalog支持     Schema支持
     * Oracle            不支持          用户名(User Id)
     * MySQL             数据库名         不支持
     */
    public List<Table> getTables(Database database) throws SQLException {
        Connection connection = null;
        ResultSet tables = null;
        try {
            // 加载转换器
            Map<String, String> convertMap = DatabaseProperties.getConvertMap();

            connection = getConnection(database.getUrl(), database.getUsername(), database.getPassword());
            DatabaseMetaData metaData = connection.getMetaData();

            String catalog = connection.getCatalog();
            String schema = connection.getSchema();
            System.err.println("连接数据库类型: " + database.getDatabaseType() + " 信息：catalog = " + catalog + " ,schema =  " + schema);
            switch (database.getDatabaseType()) {
                case MYSQL:
                    catalog = database.getName();
                    break;
                case ORACLE:
                    schema = database.getName().toUpperCase();
                    break;
                default:
                    break;
            }

            /**
             * 获取给定类别中使用的表的描述。
             * 方法原型:ResultSet getTables(String catalog,String schemaPattern,String tableNamePattern,String[] types);
             * catalog - 表所在的类别名称;""表示获取没有类别的列,null表示获取所有类别的列。
             * schema - 表所在的模式名称(oracle中对应于Tablespace);""表示获取没有模式的列,null标识获取所有模式的列; 可包含单字符通配符("_"),或多字符通配符("%");
             * tableNamePattern - 表名称;可包含单字符通配符("_"),或多字符通配符("%");
             * types - 表类型数组; "TABLE"、"VIEW"、"SYSTEM TABLE"、"GLOBAL TEMPORARY"、"LOCAL TEMPORARY"、"ALIAS" 和 "SYNONYM";null表示包含所有的表类型;可包含单字符通配符("_"),或多字符通配符("%");
             */
            tables = metaData.getTables(catalog, schema, null, new String[]{"TABLE"});

            List<Table> list = new ArrayList<Table>();
            while (tables.next()) {
                Table table = new Table();
                String tableName = tables.getString("TABLE_NAME");
                // 如果为垃圾表
                if (tableName.contains("=") || tableName.contains("$")) {
                    continue;
                }
                // 判断 表名为全大写 ，则转换为小写
//                if (tableName.toUpperCase().equals(tableName)) {
//                    table.setName(tableName.toLowerCase());
//                }
                table.setName(tableName);

                //设置表的描述信息
                table.setComment(tables.getString("REMARKS"));
                System.out.println("信息：catalog = " + catalog + " ,schema =  " + schema + " ,tableName =  " + tableName);
                // 获得数据库中表的所有主键
                List<String> keys = primaryKeys(metaData, catalog, schema, tableName);
                System.out.println("数据库中表的所有主键: " + keys);
                // 获得所有列
                List<Column> columnList = getColumns(metaData, catalog, schema, tableName, convertMap, table, keys);
                table.setColumns(columnList);
                list.add(table);
            }
            return list;
        } finally {
            close(tables);
            close(connection);
        }
    }

    /**
     * 获取所有列
     */
    private List<Column> getColumns(DatabaseMetaData metaData, String catalog, String schema, String tableName, Map<String, String> convertMap, Table table, List<String> keys) throws SQLException {
        ResultSet columns = null;
        try {
            columns = metaData.getColumns(catalog, schema, tableName, null);
            List<Column> columnList = new ArrayList<Column>();
            while (columns.next()) {
                Column column = new Column();
                String columnName = columns.getString("COLUMN_NAME");
                // 判断 列名为全大写 ，则转换为小写
                if (columnName.toUpperCase().equals(columnName)) {
                    // 转换为小写
                    columnName = columnName.toLowerCase();
                }
                column.setName(columnName);

                String dbType = columns.getString("TYPE_NAME");
                // 数据库原始类型
                column.setDbType(dbType);

                // 获取转换后的类型
                String type = convertMap.get(dbType);
                if (type == null) {
                    type = dbType;
                }
                column.setType(type);

                // 备注
                String remarks = columns.getString("REMARKS");
                if (remarks == null) {
                    remarks = columnName;
                }
                column.setComment(remarks);

                // 如果该列是主键
                if (keys.contains(columnName)) {
                    column.setKey(true);
                    table.setKey(column.getName());
                } else {
                    column.setKey(false);
                }
                // 小数位数
                int decimal_digits = columns.getInt("DECIMAL_DIGITS");
                if (decimal_digits > 0) {
                    // 如果是小数则设置为Double
                    column.setType("Double");
                }

                column.setDecimal_digits(decimal_digits);
                // 字段长度
                column.setSize(columns.getInt("COLUMN_SIZE"));

                columnList.add(column);
            }
            return columnList;
        } finally {
            close(columns);
        }
    }

    /**
     * 获取表的所有主键
     */
    private List<String> primaryKeys(DatabaseMetaData metaData, String catalog, String schema, String tableName) throws SQLException {
        ResultSet primaryKeys = null;
        try {
            primaryKeys = metaData.getPrimaryKeys(catalog, schema, tableName);
            List<String> keys = new ArrayList<String>();
            while (primaryKeys.next()) {
                String keyName = primaryKeys.getString("COLUMN_NAME");
                // 判断 主键名为全大写 ，则转换为小写
                if (keyName.toUpperCase().equals(keyName)) {
                    // 转换为小写
                    keyName = keyName.toLowerCase();
                }
                keys.add(keyName);
            }
            return keys;
        } finally {
            close(primaryKeys);
        }
    }

    /**
     * 根据配置文件获取连接
     *
     * @return
     */
    public Connection getConnection() throws SQLException {
        return getConnection(databaseProperties.getUrl().replace("[dbName]","mysql"), databaseProperties.getUsername(), databaseProperties.getPassword());
    }

    /**
     * 获取连接
     * 注意:java8+遵循了JDBC4.X规范,获取连接的时候会加载驱动,
     * 不用每次都写Class.forName()
     *
     * @return
     */
    public static Connection getConnection(String url, String username, String password) throws SQLException {
        Properties props = new Properties();
        //获取备注信息
        props.put("remarksReporting", "true");
        props.put("user", username);
        props.put("password", password);
        return DriverManager.getConnection(url, props);
    }

    /**
     * 关闭连接,释放资源
     */
    public static void close(Object o) {
        if (o == null) {
            return;
        }
        if (o instanceof ResultSet) {
            try {
                ((ResultSet) o).close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        } else if (o instanceof Statement) {
            try {
                ((Statement) o).close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        } else if (o instanceof Connection) {
            Connection c = (Connection) o;
            try {
                if (!c.isClosed()) {
                    c.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        o = null;
    }


}
